﻿function AzureTableFilterExpressionParser() {

    var self = this;

    var utils = new Utils();
    
    function isEmptyOrPunctuation(ch) {
        return ch == ' ' || ch == '\0' || ch == ')';
    }
    
    function InitState(parserContext) {

        this.accept = function (ch) {
            if (ch == '+' || ch == '-' || (ch >= '0' && ch <= '9')) {
                return new IntegerState(parserContext).accept(ch);
            } else {
                switch (ch) {
                    case '\'':
                        return new StringState(parserContext);
                    case '(':
                    case ')':
                        parserContext.addToken({
                            type: 'simple',
                            value: ch
                        });
                        break;
                    case ' ':
                    case '\0':
                        break;
                    default:
                        return new LiteralState(parserContext).accept(ch);
                }
            }
            return this;
        };
    }

    function LiteralState(parserContext) {

        var token = '';

        this.accept = function (ch) {
            switch (ch) {
                case '\'':
                    switch (token) {
                        case '':
                        case 'datetime':
                        case 'guid':
                            return new StringState(parserContext, token);
                        default:
                            parserContext.addError('Unknown literal type "' + token + '"');
                            return new StringState(parserContext);
                    }
                case '(':
                case ')':
                case ' ':
                case '\0':
                    parserContext.addToken({
                        type: 'simple',
                        value: token
                    });
                    return new InitState(parserContext).accept(ch);
                default:
                    token += ch;
                    break;
            }
            return this;
        };

    }

    function StringState(parserContext, literalType) {
        var token = '';

        function addToken() {
            parserContext.addToken({
                type: literalType || 'string',
                value: token
            });
        }
        
        this.accept = function (ch) {
            if (ch == '\'') {
                switch (literalType) {
                    case 'guid':
                        if (!validateGuid(token)) {
                            parserContext.addError('Invalid guid ' + token);
                        }
                        break;
                }
                addToken();
                return new InitState(parserContext);
            } else if (ch == '\0') {
                parserContext.addError('Unclosed string constant. \' character expected');
                addToken();
            } else {
                token += ch;
            }
            return this;
        };

    }

    function IntegerState(parserContext) {

        var state = '';
        var token = '';

        function addToken() {
            parserContext.addToken({
                type: 'integer',
                value: token
            });
        }
        
        this.accept = function (ch) {
            switch (state) {
                case '':
                    token += ch;
                    if (ch == '+' || ch == '-') {
                        state = 'postSign';
                    } else if (ch >= '0' && ch <= '9') {
                        state = 'value';
                    } else {
                        state = 'invalid';
                    }
                    break;
                case 'postSign':
                    token += ch;
                    if (ch >= '0' && ch <= '9') {
                        state = 'value';
                    } else {
                        state = 'invalid';
                    }
                    break;
                case 'value':
                    if (ch >= '0' && ch <= '9') {
                        token += ch;
                    } else if (ch == '.') {
                        token += ch;
                        return new DoubleState(parserContext, token);
                    } else if (isEmptyOrPunctuation(ch)) {
                        addToken();
                        return new InitState(parserContext).accept(ch);
                    } else {
                        token += ch;
                        state = 'invalid';
                    }
                    break;
                case 'invalid':
                    if (isEmptyOrPunctuation(ch)) {
                        parserContext.addError('Invalid integer ' + token);
                        addToken();
                        return new InitState(parserContext).accept(ch);
                    } else {
                        token += ch;
                    }
                    break;
            }
            return this;
        };
    }

    function DoubleState(parserContext, integerDotString) {

        var state = '';

        var token = integerDotString;

        function addToken() {
            parserContext.addToken({
                type: 'double',
                value: token
            });
        }
        
        this.accept = function (ch) {
            switch (state) {
                case '':
                    if (ch >= '0' && ch <= '9') {
                        token += ch;
                        state = 'fraction';
                    } else if (isEmptyOrPunctuation(ch)) {
                        parserContext.addError('Missing fraction part ' + token);
                        addToken();
                    } else {
                        token += ch;
                        state = 'invalid';
                    }
                    return this;
                case 'fraction':
                    if (ch >= '0' && ch <= '9') {
                        token += ch;
                    } else if (isEmptyOrPunctuation(ch)) {
                        addToken();
                        return new InitState(parserContext).accept(ch);
                    } else if (ch == 'e' || ch == 'E') {
                        token += ch;
                        state = 'expo';
                    } else {
                        token += ch;
                        state = 'invalid';
                    }

                    return this;
                case 'expo':
                    if (ch == '+' || ch == '-') {
                        token += ch;
                        state = 'expoValue';
                    } else if (ch >= '0' && ch <= '9') {
                        token += ch;
                        state = 'expoValue';
                    } else if (isEmptyOrPunctuation(ch)) {
                        parserContext.addError('Missing exponent part ' + token);
                        addToken();
                        return new InitState(parserContext);
                    } else {
                        token += ch;
                        state = 'invalid';
                    }
                    break;
                case 'expoValue':
                    if (ch >= '0' && ch <= '9') {
                        token += ch;
                    } else if (isEmptyOrPunctuation(ch)) {
                        addToken();
                        return new InitState(parserContext).accept(ch);
                    } else {
                        token += ch;
                        state = 'invalid';
                    }
                    break;
                case 'invalid':
                    if (isEmptyOrPunctuation(ch)) {
                        parserContext.addError('Invalid double ' + token);
                        addToken();
                        return new InitState(parserContext).accept(ch);
                    } else {
                        token += ch;
                    }
                    break;
            }

            return this;
        };
    }

    function getTokens(parserContext, str) {
        str += '\0';
        var stateObj = new InitState(parserContext);
        for (var i = 0; i < str.length; i++) {
            var ch = str.charAt(i);
            stateObj = stateObj.accept(ch);
        }
        return parserContext.tokens;
    }

    function processOperators(parserContext, tokens) {
        var result = {
            expression: null,
            errors: parserContext.errors
        };

        var stack = [];

        function checkCompare() {
            if (stack.length < 3) return false;
            var left = stack[stack.length - 3];
            var operator = stack[stack.length - 2];
            var right = stack[stack.length - 1];
            if (operator.type == 'simple' && left.type == 'simple') {
                if (right.type != 'simple' && right.type != 'string' && right.type != 'integer' && right.type != 'double' && right.type != 'datetime' && right.type != 'guid') return false;
                switch (operator.value) {
                    case 'eq':
                    case 'gt':
                    case 'ge':
                    case 'lt':
                    case 'le':
                    case 'ne':
                        stack.pop();
                        stack.pop();
                        stack.pop();

                        var expr = new AzureTableFilterCompareExpression(left.value, operator.value, right.value);
                        switch (right.type) {
                            case 'string':
                                expr.edmType = 'Edm.String';
                                break;
                            case 'datetime':
                                expr.edmType = 'Edm.DateTime';
                                break;
                            case 'guid':
                                expr.edmType = 'Edm.Guid';
                                break;
                            case 'integer':
                                expr.edmType = 'Edm.Int32';
                                break;
                            case 'double':
                                expr.edmType = 'Edm.Double';
                                break;
                            default:
                                parserContext.addError("Unknown Edm type  " + right.type);
                        }
                        stack.push(expr);
                        return true;
                }
            }
            return false;
        }

        function checkCombine() {
            if (stack.length < 3) return false;
            var left = stack[stack.length - 3];
            var operator = stack[stack.length - 2];
            var right = stack[stack.length - 1];
            if (operator.type == 'simple') {
                if (left.type != 'compare' && left.type != 'combine' && left.type != 'func') return false;
                if (right.type != 'compare' && right.type != 'combine' && right.type != 'func') return false;

                switch (operator.value) {
                    case 'and':
                    case 'or':
                        stack.pop();
                        stack.pop();
                        stack.pop();

                        stack.push(new AzureTableFilterCombineExpression(left, operator.value, right));
                        return true;
                }
            }
            return false;
        }

        function checkParentheses() {
            if (stack.length < 3) return false;
            var popen = stack[stack.length - 3];
            var content = stack[stack.length - 2];
            var pclose = stack[stack.length - 1];

            if (popen.type == 'simple' && popen.value == '(' && pclose.type == 'simple' && pclose.value == ')') {
                switch (content.type) {
                    case 'combine':
                    case 'compare':
                    case 'func':
                        stack.pop();
                        stack.pop();
                        stack.pop();
                        stack.push(content);
                        return true;
                    default:
                        parserContext.addError('Unexpected expression between \'(\' and \')\'');
                        break;
                }
            }
            return false;
        }

        function checkFunctions() {
            if (stack.length < 4) return false;
            var funcName = stack[stack.length - 4];
            var popen = stack[stack.length - 3];
            var funcArg = stack[stack.length - 2];
            var pclose = stack[stack.length - 1];
            if (popen.type == 'simple' && popen.value == '(' && pclose.type == 'simple' && pclose.value == ')' && funcName.type == 'simple') {
                if (funcName.value == 'not') {
                    stack.pop();
                    stack.pop();
                    stack.pop();
                    stack.pop();
                    stack.push(new AzureTableFilterFuncExpression(funcName.value, funcArg));
                    return true;
                }
            }
            return false;
        }

        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            stack.push(token);

            var handled = true;

            do {
                if (checkFunctions()) continue;
                if (checkCompare()) continue;
                if (checkCombine()) continue;
                if (checkParentheses()) continue;
                handled = false;
            } while (handled)
        }

        if (stack.length == 1) {
            var res = stack[0];
            if (res.type == 'simple' || res.type == 'string' || res.type == 'integer' || res.type == 'double' || res.type == 'guid' || res.type == 'datetime') {
                parserContext.addError("Expression is not complete");
            }
            result.expression = res;
            return result;
        }
        parserContext.addError("Expression is invalid");
        return result;
    }

    function createParserContext() {

        var errors = [];
        var tokens = [];

        function addError(msg) {
            errors.push(msg);
        }

        function addToken(token) {
            tokens.push(token);
        }

        return {
            errors: errors,
            tokens: tokens,
            addError: addError,
            addToken: addToken
        };
    }

    function parse(str) {
        var res = parseQuery(str);
        if (!res) return null;
        if (res.errors.length > 0) {
            throw new Error(res.errors[0]);
        }
        return res.expression;
    }

    function parseQuery(str) {
        if (utils.isNullOrWhiteSpace(str)) {
            return null;
        }
        var parserContext = createParserContext();
        var tokens = getTokens(parserContext, str);
        return processOperators(parserContext, tokens);
    }

    function validateGuid(str) {
        str = str.trim();
        if (str.charAt(0) == '{') {
            if (str.charAt(str.length - 1) != '}') {
                return false;
            }
            str = str.substr(1, str.length - 2);
        }

        function allHex(start, end) {
            for (var i = start; i <= end; i++) {
                var ch = str.charAt(i);
                if (ch >= '0' && ch <= '9') continue;
                if (ch >= 'A' && ch <= 'F') continue;
                if (ch >= 'a' && ch <= 'f') continue;
                return false;
            }
            return true;
        }

        if (!allHex(0, 7)) return false;
        if (str.charAt(8) != '-') return false;
        if (!allHex(9, 12)) return false;
        if (str.charAt(13) != '-') return false;
        if (!allHex(14, 17)) return false;
        if (str.charAt(18) != '-') return false;
        if (!allHex(19, 22)) return false;
        if (str.charAt(23) != '-') return false;
        if (!allHex(24, 35)) return false;
        return true;
    }

    self.parse = parse;
    self.parseQuery = parseQuery;

}